using GLib;
using Posix;
using SerialPort;

public class SerialPortClass : GLib.Object
{
    private Port _port;

    public string name { get; construct set; }

    public string newline { get; set; default = "\r\n"; }

    public DataInputStream @is;
    
    private Config _cfg;

    private Thread<int> watch_thread;

    private Cancellable cancellable;

    private File tmp_file;

    private FileIOStream io;

    private DataInputStream serialport_is;

    private DataOutputStream @os;

    private int _baudrate;

    public int baudrate {

        get { return this._baudrate; }

        set
        {
            if(Return.OK == this._cfg.set_baudrate(value))
            {
                this._baudrate = value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private int _bits;

    public int bits {
    
        get { return this._bits; }
        set
        {
            if(Return.OK == this._cfg.set_bits(value))
            {
                this._bits = value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private int _stopbits;

    public int stopbits {

        get { return this._stopbits; }

        set
        {
            if(Return.OK == this._cfg.set_stopbits(value))
            {
                this._stopbits = value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private Parity _parity;

    public Parity parity {

        get { return this._parity; }

        set
        {
            if(Return.OK == this._cfg.set_parity((Parity)value))
            {
                this._parity = (Parity)value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private Rts _rts;

    public Rts rts {

        get { return this._rts; }

        set
        {
            if(Return.OK == this._cfg.set_rts((Rts)value))
            {
                this._rts = (Rts)value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private Cts _cts;

    public Cts cts {

        get { return this._cts; }

        set
        {
            if(Return.OK == this._cfg.set_cts((Cts)value))
            {
                this._cts = (Cts)value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private Dtr _dtr;

    public Dtr dtr {
    
        get { return this._dtr; }
        
        set
        {
            if(Return.OK == this._cfg.set_dtr((Dtr)value))
            {
                this._dtr = (Dtr)value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private Dsr _dsr;

    public Dsr dsr {
    
        get{ return this._dsr; }
        
        set
        {
            if(Return.OK == this._cfg.set_dsr((Dsr)value))
            {
                this._dsr = (Dsr)value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private XonXoff _xonxoff;

    public XonXoff xonxoff {
    
        get { return this._xonxoff; }
        
        set
        {
            if(Return.OK == this._cfg.set_xonxoff((XonXoff)value))
            {
                this._xonxoff = (XonXoff)value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private FlowControl _flowcontrol;

    public FlowControl flowcontrol {
    
        get { return this._flowcontrol; }
        
        set
        {
            if(Return.OK == this._cfg.set_flowcontrol((FlowControl)value))
            {
                this._flowcontrol = (FlowControl)value;

                this._port.set_config(this._cfg);
            }
        }
    }

    private void _init_properties()
    {
        this._port.get_config(this._cfg);

        this._cfg.get_baudrate(out this._baudrate);

        this._cfg.get_bits(out this._bits);

        this._cfg.get_stopbits(out this._stopbits);

        this._cfg.get_parity(out this._parity);

        this._cfg.get_rts(out this._rts);

        this._cfg.get_cts(out this._cts);

        this._cfg.get_dtr(out this._dtr);

        this._cfg.get_dsr(out this._dsr);

        this._cfg.get_xonxoff(out this._xonxoff);
    }

    private int _watch_thread()
    {
        int bytes_read = 0;
        uint8 buffer[1024] = {0};
        EventSet event_set;

        var os = this.io.output_stream as FileOutputStream;

        EventSet.@new(out event_set);

        event_set.add_port(this._port, EventMask.RX_READY);

        do
        {
            if(Return.OK == event_set.wait(500))
            {
                try
                {                                        
                    bytes_read = this._port.nonblocking_read(buffer);
                    
                    if(bytes_read > 0)
                    {
                        uint8[] data = buffer;
                        
                        data.length = bytes_read;
                        
                        os.write(data, this.cancellable);
                        
                        try
                        {
                            os.flush(this.cancellable);
                        }
                        catch(Error error)
                        {
                            print("@flush fail : %s", error.message);
                        }
                    }
                }
                catch(IOError error)
                {
                    print("@read fail : %s", error.message);
                }
            }
        }
        while(!this.cancellable.is_cancelled());

        return 0;
    }

    public SerialPortClass(string name)
    {
        Object(name : name);

        if (Return.OK != Port.new_by_name(name, out this._port))
        {
            print("The serialport %s not exist in your system.\n", name);
        }
    }

    public bool open()
    {
        int fd = 0;

        if (Return.OK != this._port.open(OpenMode.READ_WRITE))
        {
            print("Open the serialport %s fail, please insure it exist in your system and not be in used.\n", name);
            return false;
        }

        if(Return.OK != Config.@new(out this._cfg))
        {
            print("Get the serialport %s config fail.\n", name);
            return false;
        }

        _init_properties();

        this._port.get_fd(out fd);

        try
        {
            this.tmp_file = File.new_tmp("serialport-XXXXXXXX.io", out this.@io);
        }
        catch(Error error)
        {
            print ("SerialPort create tmp file fail, %s\n", error.message);
            return false;
        }

#if OS_WIN32
        this.serialport_is = new GLib.DataInputStream(new GLib.Win32InputStream((void *)fd, false));
#else
        this.serialport_is = new GLib.DataInputStream(new GLib.UnixInputStream(fd, false));
#endif
        try
        {
            this.@is = new GLib.DataInputStream(File.new_for_path(this.tmp_file.get_path()).read() as InputStream);
        }
        catch(Error error)
        {
            print ("SerialPort open tmp fail, %s\n", error.message);
            return false;
        }

        this.@is.newline_type = DataStreamNewlineType.CR_LF;

#if OS_WIN32
        this.@os = new GLib.DataOutputStream(new GLib.Win32OutputStream((void *)fd, false));
#else
        this.@os = new GLib.DataOutputStream(new GLib.UnixOutputStream(fd, false));
#endif
        this.cancellable = new Cancellable();

        try
        {
            this.watch_thread = new Thread<int>.try ("SerialPort Watch Data", this._watch_thread);
        }
        catch(Error error)
        {
            print ("SerialPort try new thread fail, %s\n", error.message);
            return false;
        }

        return true;
    }

    public void config_set(string option, int @value)
    {
        Value tmp = Value (typeof (int));

        tmp.set_int(@value);

        this.set_property(option, tmp);
    }

    public bool close()
    {
        try
        {
            this.tmp_file.delete();
        }
        catch(Error error)
        {
            print("Serialport delete tmp file fail, %s\n", error.message);
        }

        this.cancellable.cancel();
        
        this.watch_thread.join();
        
        if(Return.OK != this._port.close())
        {
            return false;
        }
        
        return true;
    }
    
    public bool flush()
    {
        if(Return.OK != this._port.flush(Buffer.BOTH))
        {
            return false;
        }
        
        return true;
    }

    public static string[] @enum()
    {
        string[] list = {};

        var ports = Port.@enum();

        foreach(unowned Port port in ports)
        {
            list += port.name();
        }

        return list;
    }

    public string read_line(Cancellable cancellable)
    {
        try
        {
            return this.@is.read_line(null, cancellable);
        }
        catch(IOError error)
        {
            print("@is.read_line, error : %s", error.message);
        }

        return (string)null;
    }

    public bool write_line(string line)
    {
        return this.write_data((line + this.newline).data);
    }

    public bool write_data(uint8[] data)
    {
        bool res = false;
        size_t bytes_written;

        try
        {
            res = this.@os.write_all(data, out bytes_written, null);
        }
        catch (IOError error)
        {
            print("@os.write_all, error : %s\n", error.message);
        }

        return res;
    }
    
    private void get_signal(out SerialPort.Signal signals)
    {
        this._port.get_signal(out signals);
    }

    public bool is_dsr_unset()
    {
        SerialPort.Signal signals;
        
        this.get_signal(out signals);
        
        if((signals & SerialPort.Signal.DSR) == 0)
        {
            return true;
        }
        
        return false;
    }

    public string to_string()
    {
        return """===>Name : %s
===>Baudrate : %s
===>Bits: %s
===>StopBits: %s
===>Parity : %s
===>Rts : %s
===>Cts : %s
===>Dtr : %s
===>Dsr : %s
===>XonXoff : %s
===>FlowControl : %s

""".printf(
            this.name,
            this._baudrate.to_string(),
            this._bits.to_string(),
            this._stopbits.to_string(),
            this._parity.to_string(),
            this._rts.to_string(),
            this._cts.to_string(),
            this._dtr.to_string(),
            this._dsr.to_string(),
            this._xonxoff.to_string(),
            this._flowcontrol.to_string()
       );
    }
}
